Skip to content

Add appguardrail serve: multi-tenant control-plane API (platform seed)#198

Merged
seonghobae merged 11 commits into
developfrom
control-plane
Jul 10, 2026
Merged

Add appguardrail serve: multi-tenant control-plane API (platform seed)#198
seonghobae merged 11 commits into
developfrom
control-plane

Conversation

@seonghobae

Copy link
Copy Markdown
Contributor

Summary

The lever a CLI alone can't be: a persistent, multi-tenant surface. Instead of a one-shot scan that prints and exits, an org pushes every CI scan to a store and queries its history/trend over time — the continuous-monitoring, recurring-revenue backbone. Stdlib only (sqlite3 + http.server), so it ships in the same wheel with no new dependency.

  • appguardrail_core/controlplane.py — SQLite store (orgs + scans), API-key auth (sha256-hashed keys), tenant-scoped add/list/get (cross-org reads return nothing), and an HTTP API. Severity counts and deploy-blocking totals are computed from the findings on ingest.
  • appguardrail serve [--db --host --port] [--create-org NAME] — provisions an org + API key; bootstraps a default org + key on an empty DB so it runs out of the box.
  • Endpoints: POST /api/v1/scans (ingest {findings:[...], repo?, commit?}), GET /api/v1/scans (history), GET /api/v1/scans/{id} (full), GET /api/v1/health. Auth: Authorization: Bearer <api_key>.
  • Composes with the existing CLI: CI runs appguardrail scan --findings-json then POSTs the result.

SQLite + stdlib is the deliberate ponytail starting point — swap the store for a managed database behind the same functions when scale demands it.

Test plan

  • pytest — 215 passed, incl. tests/test_controlplane.py (store add/list/get + counts, tenant isolation, health without auth, ingest→history round-trip, 401 on missing/wrong key, 400 on a bad body) + module self-check
  • e2e: serve --create-org prints a key → POST /api/v1/scans (authed) → GET /api/v1/scans returns history; missing/wrong key → 401

Follow-ups: an org web dashboard consuming this API, scheduled re-scan / drift alerts, org-scoped RBAC.

🤖 Generated with Claude Code

seonghobae and others added 5 commits July 6, 2026 06:23
…m seed)

The $2B unlock a CLI can't be: a persistent, multi-tenant surface. Instead of a
one-shot scan, an org pushes every CI scan and queries its history/trend — the
recurring-revenue backbone. Stdlib only (sqlite3 + http.server), ships in the
same wheel, no dependency.

- appguardrail_core/controlplane.py: SQLite store (orgs + scans), API-key auth
  (sha256-hashed), tenant-scoped add/list/get, and an HTTP API. Counts and
  deploy-blocking are computed from findings on ingest.
- `appguardrail serve [--db --host --port] [--create-org NAME]`; bootstraps a
  default org + API key on an empty DB.
- Endpoints: POST/GET /api/v1/scans, GET /api/v1/scans/{id}, GET /api/v1/health;
  Authorization: Bearer <key>.
- Tests: tests/test_controlplane.py (store, counts, tenant isolation, health,
  ingest+history round-trip, 401 auth, 400 bad body) + module self-check.
- Verified: full suite 215 passed; e2e provision -> POST scan -> GET history,
  401 on missing/wrong key, cross-tenant reads blocked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EywwS2Du8pimW7xqRP3An3
Gives the control-plane API a user-facing surface: an org's security posture
over time, not just JSON endpoints.

- scanner/dashboard/console.html: single static page (no framework/build) that
  connects with an API key and renders scan history, the deploy-blocking trend
  (per-scan bars), and per-scan finding detail. Reuses the design tokens; all
  interpolation HTML-escaped.
- controlplane.py: the server serves the console at `/`, `/console`,
  `/index.html` (packaged via importlib.resources, dashboard/*.html).
- Tests: console served at root (200 + marker).
- Verified: full suite 216 passed; puppeteer e2e — connect with key, history +
  trend + detail render, a CRITICAL finding shows in scan detail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EywwS2Du8pimW7xqRP3An3
Turns the platform into continuous monitoring: CI pushes each scan and the
control plane reports what got *worse* since last time.

- controlplane.py: add_scan computes `new_blocking` — deploy-blocking findings
  new since this org+repo's previous scan (line-independent fingerprint), stored
  and returned. list/get expose it.
- `appguardrail scan --push <url>`: POST normalized findings to
  /api/v1/scans (key from APPGUARDRAIL_API_KEY; repo/commit from
  GITHUB_REPOSITORY/GITHUB_SHA). Missing key / push failure degrades gracefully
  and never fails the scan.
- console.html: surfaces "New since last scan" as a stat and a per-scan drift
  column.
- Tests: drift (first scan all-new, moved-line not-new, newly-introduced counted,
  per-repo baseline).
- Verified: full suite 217 passed; e2e scan --push -> control plane ingests with
  drift; missing key skips gracefully.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EywwS2Du8pimW7xqRP3An3
Completes the CI integration: the installed `appguardrail monitor` workflow now
pushes each scan to a control plane when configured, so history + drift populate
automatically — with zero change for users who don't set it up.

- MONITOR_WORKFLOW: job-level CP_URL from the APPGUARDRAIL_CONTROL_PLANE_URL
  secret; the scan step adds `--push $CP_URL` only when it's set (SARIF upload +
  deploy gate unchanged). APPGUARDRAIL_API_KEY passed from secrets.
- Updated the monitor-workflow test to assert the push wiring.
- Verified: full suite 217 passed; `appguardrail monitor` installs the workflow
  with the conditional push step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EywwS2Du8pimW7xqRP3An3
Closes detect -> alert. Drift was visible but passive; now the control plane
pushes an alert the moment CI introduces a new deploy-blocking finding, which is
the action teams actually pay for.

- controlplane.py: orgs gain a webhook_url; set_webhook() and a POST
  /api/v1/webhook endpoint set it. add_scan fires _send_alert() (best-effort,
  never fails ingest) with an event payload only when new_blocking > 0.
- Tests: alert fires on new blockers and not on a moved/unchanged finding, no
  webhook -> no delivery, API sets the webhook.
- Verified: full suite 220 passed; e2e against a local receiver — alerts land
  only for scans that introduce new blockers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EywwS2Du8pimW7xqRP3An3
seonghobae and others added 2 commits July 6, 2026 07:26
…plane (#201)

Multi-tenant needs multi-user: an org shares one key today, so CI, a read-only
dashboard, and an admin all hold the same all-powerful credential. This adds
per-key roles.

- controlplane.py: `keys` table (org_id, key_hash, role, label). create_key()
  issues a role-scoped key; role_for_key() resolves (org_id, role) and treats
  the bootstrap key as owner; has_role() enforces viewer<member<owner.
  create_org now seeds an owner key.
- API enforcement: GET (read) = any role; POST /scans = member+; POST /webhook
  and POST /keys = owner. New POST /api/v1/keys mints role-scoped keys.
- Tests: role resolution + has_role ordering, and the full API role matrix
  (viewer read-only, member ingest-only, owner full) via the live server.
- Verified: full suite 236 passed; e2e role matrix (viewer/member/owner) enforced.


Claude-Session: https://claude.ai/code/session_01EywwS2Du8pimW7xqRP3An3

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Scan history grows unbounded; the API returned everything and the console had
no first-class trend series. Adds paging and a chart-ready trend.

- controlplane.py: list_scans() gains an offset; new scan_trend() returns an
  oldest->newest deploy_blocking/new_blocking series. GET /api/v1/scans accepts
  ?limit=&offset=; new GET /api/v1/scans/trend?limit=.
- Tests: pagination offset windows + trend ordering/shape, store and API level.
- Verified: full suite passed; e2e limit/offset paging + trend series.


Claude-Session: https://claude.ai/code/session_01EywwS2Du8pimW7xqRP3An3

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread appguardrail_core/controlplane.py Fixed
Comment thread tests/test_controlplane.py Fixed
Comment thread appguardrail_core/controlplane.py
Comment thread appguardrail_core/controlplane.py Fixed
When an org's drift-alert webhook URL is a Slack Incoming Webhook
(host hooks.slack.com), render the alert as a Block Kit message:
a header with the new deploy-blocking count, a fields section for
org / new blockers / repo / scan id, and a section listing the top
5 offending rule_id/file pairs with a "+N more" overflow line.
Text is Slack-escaped and length-trimmed to Block Kit caps.

Any non-Slack URL still receives the existing generic JSON payload
unchanged (backward compatible). The format is chosen inside
_send_alert via host detection; add_scan now passes the org name and
the list of newly-introduced blocking findings through so the Slack
renderer can name them. No new dependency (stdlib urllib only).


Claude-Session: https://claude.ai/code/session_01EywwS2Du8pimW7xqRP3An3

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial review of the control plane found two real gaps, both reachable
by any holder of a valid key:

- Request bodies were unbounded: a huge Content-Length OOMs the server, and a
  negative one makes rfile.read(-1) block until EOF. Now capped at 10MiB and
  negatives rejected (400) before reading.
- limit/offset query ints were passed through unclamped: sqlite treats
  LIMIT -1 as *no limit*, so ?limit=-1 dumped the org's entire scan history,
  bypassing the pagination cap. Now clamped (list 1..1000, trend 1..365,
  offset >= 0).

Reviewed-and-cleared in the same pass: role validation on key creation (safe
fallback to member), unknown-role handling in has_role (rank -1), key lookup
by sha256 hash (no timing-sensitive comparison), tenant isolation on all
reads (existing tests).

Tests: 3 new (limit=-1 / huge limit+negative offset clamped; 50MiB body ->
400; negative Content-Length -> 400). Full suite 245 passed.


Claude-Session: https://claude.ai/code/session_01EywwS2Du8pimW7xqRP3An3

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@opencode-agent opencode-agent Bot disabled auto-merge July 6, 2026 20:06
seonghobae and others added 2 commits July 9, 2026 13:40
SHA-256 is a fast, non-memory-hard hash unsuitable for hashing secrets:
if the control-plane store leaks, an attacker can brute-force API keys at
billions of guesses per second. Switch _hash_key to hashlib.scrypt, a
memory-hard KDF from the stdlib, with a fixed application salt so the hash
stays deterministic and keys remain findable by indexed equality lookup.

Resolves CodeQL py/weak-sensitive-data-hashing (high).
# Conflicts:
#	CHANGELOG.md
#	README.md
#	scanner/cli/appguardrail.py
@opencode-agent

opencode-agent Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

OpenCode Review Overview

  • Head SHA: cb0b859d14ffc078f593c035ee1cb2866c50dbaa
  • Workflow run: 29086877461
  • Workflow attempt: 1
  • Gate result: APPROVE (approval step)

Pull request overview

OpenCode reviewed the current-head bounded evidence and found no blocking issues.

Findings

No blocking findings.

Summary

Approval sufficiency: bounded evidence supplied affirmative approval evidence for changed files, coverage/docstring posture, risk surfaces, and current-head verification; approval is not based merely on the absence of known blockers.
Verification posture: CodeGraph evidence was initialized and bounded current-head evidence reviewed for changed-file evidence including CHANGELOG.md, README.md, appguardrail_core/controlplane.py, scanner/cli/appguardrail.py, scanner/dashboard/console.html, and 2 more.
Linter/static: workflow/static review evidence is bounded by the current-head GitHub Checks gate and changed-file evidence.
TDD/regression: coverage execution evidence and focused changed hunks were reviewed from bounded-review-evidence.md.
Coverage: coverage execution evidence reports supported repository test suites passed.
Docstring coverage: coverage execution evidence reports configured repository docstring gates passed or docstring coverage was advisory.
DAG: CodeGraph/source-backed behavior map connects CHANGELOG.md to the affected review, runtime, or workflow path and required checks.
PoC/execution: coverage-evidence job executed on the current head and reported PASS.
DDD/domain: workflow and repository-governance invariants were reviewed against changed files in bounded evidence.
CDD/context: CodeGraph evidence, changed-file history, and focused hunks were reviewed from bounded-review-evidence.md.
Similar issues: changed-file history evidence was reviewed for comparable local precedents.
Claim/concept check: bounded evidence, repository source, current-head workflow evidence, and, where numeric, scientific, statistical, or literature-backed claims are affected, original-paper/formula evidence and parameter-recovery expectations were used for claims.
Standards search: standards and external-source checks are delegated to configured OpenCode web_search/Context7/DeepWiki sources when applicable; no evidence-backed standards blocker is present in bounded evidence.
Compatibility/convention: changed workflow/script conventions, object naming, and reserved-word safety for schema/API/config/code surfaces were checked in bounded evidence.
Breaking-change/backcompat: deployment evidence and changed-file history were checked for backward-compatibility risk.
Performance: changed surfaces were checked for performance risk in bounded evidence.
Developer experience: changed automation, review, test, setup, and maintenance surfaces were checked for helpful or obstructive DX impact in bounded evidence.
User experience: connected user, operator, API, CLI, documentation, review-comment, status-check, rendering, and workflow-reader behavior was checked for contradictions against code, docs, and tests in bounded evidence.
Visual/DOM: Playwright visual, DOM locator, ARIA snapshot, console, and responsive evidence were checked when a web UI surface was present; for non-web surfaces, API/CLI/log/docs/workflow interaction evidence was reviewed instead.
Accessibility/i18n: accessibility, localization, and human-readable text surfaces were checked where UI, CLI, API message, docs, logs, or review text changed.
Supply-chain/license: dependency, package, model, container, and external-tool changes were checked in bounded evidence.
Packaging: package, build, test, lint, and security contracts were checked in bounded evidence.
Security/privacy: workflow-token, review-gate, and repository-automation security/privacy boundaries were checked in bounded evidence.

  • Result: APPROVE
  • Reason: PR introduces a well-tested, secure, and maintainable multi-tenant control-plane API with no unresolved issues.
  • Head SHA: cb0b859d14ffc078f593c035ee1cb2866c50dbaa
  • Workflow run: 29086877461
  • Workflow attempt: 1

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file (5 files)"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file (5 files)"]
  R1 --> V1["required checks"]
  Evidence --> S2["Test (2 files)"]
  S2 --> I2["regression suite"]
  I2 --> R2["Review risk: Test (2 files)"]
  R2 --> V2["targeted test run"]
Loading

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

OpenCode reviewed the current-head bounded evidence and found no blocking issues.

Findings

No blocking findings.

Summary

Approval sufficiency: bounded evidence supplied affirmative approval evidence for changed files, coverage/docstring posture, risk surfaces, and current-head verification; approval is not based merely on the absence of known blockers.
Verification posture: CodeGraph evidence was initialized and bounded current-head evidence reviewed for changed-file evidence including CHANGELOG.md, README.md, appguardrail_core/controlplane.py, scanner/cli/appguardrail.py, scanner/dashboard/console.html, and 2 more.
Linter/static: workflow/static review evidence is bounded by the current-head GitHub Checks gate and changed-file evidence.
TDD/regression: coverage execution evidence and focused changed hunks were reviewed from bounded-review-evidence.md.
Coverage: coverage execution evidence reports supported repository test suites passed.
Docstring coverage: coverage execution evidence reports configured repository docstring gates passed or docstring coverage was advisory.
DAG: CodeGraph/source-backed behavior map connects CHANGELOG.md to the affected review, runtime, or workflow path and required checks.
PoC/execution: coverage-evidence job executed on the current head and reported PASS.
DDD/domain: workflow and repository-governance invariants were reviewed against changed files in bounded evidence.
CDD/context: CodeGraph evidence, changed-file history, and focused hunks were reviewed from bounded-review-evidence.md.
Similar issues: changed-file history evidence was reviewed for comparable local precedents.
Claim/concept check: bounded evidence, repository source, current-head workflow evidence, and, where numeric, scientific, statistical, or literature-backed claims are affected, original-paper/formula evidence and parameter-recovery expectations were used for claims.
Standards search: standards and external-source checks are delegated to configured OpenCode web_search/Context7/DeepWiki sources when applicable; no evidence-backed standards blocker is present in bounded evidence.
Compatibility/convention: changed workflow/script conventions, object naming, and reserved-word safety for schema/API/config/code surfaces were checked in bounded evidence.
Breaking-change/backcompat: deployment evidence and changed-file history were checked for backward-compatibility risk.
Performance: changed surfaces were checked for performance risk in bounded evidence.
Developer experience: changed automation, review, test, setup, and maintenance surfaces were checked for helpful or obstructive DX impact in bounded evidence.
User experience: connected user, operator, API, CLI, documentation, review-comment, status-check, rendering, and workflow-reader behavior was checked for contradictions against code, docs, and tests in bounded evidence.
Visual/DOM: Playwright visual, DOM locator, ARIA snapshot, console, and responsive evidence were checked when a web UI surface was present; for non-web surfaces, API/CLI/log/docs/workflow interaction evidence was reviewed instead.
Accessibility/i18n: accessibility, localization, and human-readable text surfaces were checked where UI, CLI, API message, docs, logs, or review text changed.
Supply-chain/license: dependency, package, model, container, and external-tool changes were checked in bounded evidence.
Packaging: package, build, test, lint, and security contracts were checked in bounded evidence.
Security/privacy: workflow-token, review-gate, and repository-automation security/privacy boundaries were checked in bounded evidence.

  • Result: APPROVE
  • Reason: PR introduces a well-tested, secure, and maintainable multi-tenant control-plane API with no unresolved issues.
  • Head SHA: cb0b859d14ffc078f593c035ee1cb2866c50dbaa
  • Workflow run: 29086877461
  • Workflow attempt: 1

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file (5 files)"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file (5 files)"]
  R1 --> V1["required checks"]
  Evidence --> S2["Test (2 files)"]
  S2 --> I2["regression suite"]
  I2 --> R2["Review risk: Test (2 files)"]
  R2 --> V2["targeted test run"]
Loading

@seonghobae seonghobae merged commit f64221c into develop Jul 10, 2026
26 checks passed
@seonghobae seonghobae deleted the control-plane branch July 10, 2026 10:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants